﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Windows.Forms;
using SharpSvn;
using SourceSafeTypeLib;

namespace SubVisual
{
    internal class Migrator
    {
        private readonly List<IVSSItem> fileList = new List<IVSSItem>();
        private readonly List<IVSSItem> projList = new List<IVSSItem>();
        private int numFilesHandled;
        private int numRevisionsHandled;
        private string outputDIR;
        private string repoDIR;
        private string svnREVPROPSPATH;
        private string svnURL;
        private VSSDatabase vssDb;
        private SvnClient svnClient;
        private IWin32Window owner;
        private bool allowExistingDirectories; // TODO: Find a way to import into existing directories?
        private bool running = false;
        private bool cancelled = false;
        private string errorMessage;
        private string errorDetail;
        private string lastResult;
        private string lastResultDetails;

        public bool AllowExistingDirectories
        {
            get { return allowExistingDirectories; }
            set { allowExistingDirectories = value; }
        }

        public bool Cancelled
        {
            get { return cancelled; }
        }

        public string LastResult
        {
            get { return lastResult; }
        }

        public string LastResultDetails
        {
            get { return lastResultDetails; }
        }

        public string ErrorMessage
        {
            get
            {
                if (string.IsNullOrEmpty(errorMessage))
                    return "Unknown error";
                else
                    return errorMessage;
            }
        }

        public string ErrorDetail
        {
            get { return errorDetail; }
        }

        public Migrator(VSSDatabase source, String target, String revPropsPath, IWin32Window owner)
        {
            this.vssDb = source;
            this.svnURL = target;
            if (!svnURL.EndsWith("/"))
            {
                svnURL += "/";
            }
            this.svnClient = GetSvnClient();
            this.owner = owner;

            this.repoDIR = Path.Combine(Path.GetTempPath(), "_migrate");
            Directory.CreateDirectory(this.repoDIR);
            this.outputDIR = Path.Combine(this.repoDIR, DateTime.Now.ToString().Replace(":", "_"));
            Directory.CreateDirectory(this.outputDIR);

            this.svnREVPROPSPATH = revPropsPath;
        }

        public event System.ComponentModel.ProgressChangedEventHandler ProgressChanged;

        public void ReportProgress(int percentCompleted, String message)
        {
            System.ComponentModel.ProgressChangedEventHandler temp = ProgressChanged;
            if (temp != null)
                temp(this, new System.ComponentModel.ProgressChangedEventArgs(percentCompleted, message));
        }

        public bool CancelExecution()
        {
            if (running)
                cancelled = true;
            return cancelled;
        }

        public bool Execute(List<IVSSItem> projList)
        {
            try
            {
                running = true;

                this.projList.Clear();
                DateTime startTime = DateTime.Now;

                Cleanup();

                if (!BuildFileList(projList, fileList))
                {
                    if(String.IsNullOrEmpty(errorMessage))
                        errorMessage="Unknown error while building file list";
                    return false;
                }

                if (cancelled)
                    return false;

                if (!CreateDirectories(this.projList))
                {
                    if(String.IsNullOrEmpty(errorMessage))
                        errorMessage="Unknown error while creating directory structure";
                    return false;
                }

                if (cancelled)
                    return false;

                if (!ImportDirectories(projList))
                {
                    if(String.IsNullOrEmpty(errorMessage)&&!cancelled)
                        errorMessage="Unknown error while importing directories";
                    return false;
                }

                if (cancelled)
                    return false;

                if (!GetAndAddFiles())
                {
                    if(String.IsNullOrEmpty(errorMessage)&&!cancelled)
                        errorMessage="Unknown error while adding files";
                    return false;
                }
                TimeSpan t = DateTime.Now.Subtract(startTime);
                lastResult = "Migration performed succesfully\n\n";
                lastResultDetails = String.Format("Duration: {0:00}:{1:00}:{2:00}\n", t.Hours, t.Minutes, t.Seconds);
                lastResultDetails += String.Format("Files migrated: {0}\n", numFilesHandled);
                lastResultDetails += String.Format("File revisions migrated: {0}\n", numRevisionsHandled);

                return true;
            }
            catch (Exception e)
            {
                errorMessage = e.Message;
                return false;
            }
            finally
            {
                running = false;
            }
        }

        private bool BuildFileList(List<IVSSItem> projectList, List<IVSSItem> fileList)
        {
            ReportProgress(0, "Building file list");
            int projectsProcessed = 0;

            foreach (IVSSItem vssItem in projectList)
            {
                ReportProgress(5*++projectsProcessed/projectList.Count, "Building file list");
                if (vssItem.Type == (int)VSSItemType.VSSITEM_PROJECT)
                {
                    if(!BuildFileList(vssItem, fileList))
                        return false; ;
                }
                else
                {
                    fileList.Add(vssItem);
                }
            }
            if (fileList.Count == 0)
            {
                errorMessage="No results from building file list";
                return false;
            }
            return true;
        }

        private bool BuildFileList(IVSSItem fromVssProject, List<IVSSItem> fileList)
        {
            projList.Add(fromVssProject);

            foreach (IVSSItem vssItem in fromVssProject.get_Items(false))
            {
                if (vssItem.Type == (int)VSSItemType.VSSITEM_PROJECT)
                {
                    if(!BuildFileList(vssItem, fileList))
                        return false;
                }
                else
                {
                    // skip VSS metadata files.
                    if (!Path.GetExtension(vssItem.Name).Contains("scc"))
                    {
                        fileList.Add(vssItem);
                    }
                }
            }
            return true;
        }

        private bool CreateDirectories(List<IVSSItem> projList)
        {
            int directoriesProcessed = 0;

            foreach (IVSSItem vssProj in projList)
            {
                ReportProgress(5 + 5 * ++directoriesProcessed / projList.Count, "Creating directories");
                string dir = Path.Combine(repoDIR, vssProj.Spec.Substring(2).Replace("/", "\\"));
                Directory.CreateDirectory(dir);
            }
            return true;
        }

        private bool ImportDirectories(List<IVSSItem> projects)
        {
            int directoriesImported = 0;
            foreach (IVSSItem project in projects)
            {
                if (cancelled)
                    return false;
                ReportProgress(10 + 5 * ++directoriesImported / projects.Count, "Importing directories");
                if (!ImportDirectories(project))
                    return false;
            }
            return true;
        }
        private bool ImportDirectories(IVSSItem project)
        {
            var uri = new Uri(string.Format("{0}{1}", svnURL, project.Name));
            string dir = Path.Combine(repoDIR, project.Spec.Substring(2).Replace("/", "\\"));

            var importArgs = new SvnImportArgs { LogMessage = "Initial import" };
            try
            {
                if (!svnClient.Import(dir, uri, importArgs)) return false;
            }
            catch (SvnFileSystemException sfse)
            {
                if (sfse.SvnErrorCode == SvnErrorCode.SVN_ERR_FS_ALREADY_EXISTS)
                {
                    errorMessage = "The path " + uri + " already exists in SVN repository. Please choose a new location or delete that directory.";
                    return false;
                }
                throw sfse;
            }

            var checkOutArgs = new SvnCheckOutArgs { Depth = SvnDepth.Infinity };
            return svnClient.CheckOut(uri, dir, checkOutArgs);
        }

        private bool GetAndAddFiles()
        {
            foreach (IVSSItem vssFile in fileList)
            {
                numFilesHandled++;

                IVSSVersions vssVersions = vssFile.get_Versions(0);
                if(!GetAndAddFile(vssFile, vssVersions, svnClient))
                    return false;
            }
            return true;
        }

        private bool GetAndAddFile(IVSSItem vssFile, IVSSVersions vssVersions, SvnClient svnClient)
        {
            List<IVSSVersion> versions = new List<IVSSVersion>();
            foreach(IVSSVersion vssVersion in vssVersions)
            {
                if (!string.IsNullOrEmpty(vssVersion.Label) || !string.IsNullOrEmpty(vssVersion.LabelComment))
                {
                    continue; //skip labels
                }
                versions.Add(vssVersion);
            }

            versions.Reverse();

            int versionCounter = 0;
            string versionMessage = string.Empty;
            bool isAdd = true;
            foreach (IVSSVersion version in versions)
            {
                ReportProgress(15 + 85 * numFilesHandled / fileList.Count, String.Format("Importing [{0}/{1}] {2}, version {3} of {4}", new object[] {numFilesHandled, fileList.Count, vssFile.Spec.Substring(2), ++versionCounter, versions.Count}));
                if (!GetAndAddFileVersion(vssFile, version, svnClient, isAdd))
                    return false;
                isAdd = false;
            }
            return true;
        }

        private bool GetAndAddFileVersion(IVSSItem vssFile, IVSSVersion vssVersion, SvnClient svnClient,
                                                 bool isAdd)
        {
            if (cancelled)
                return false;

            numRevisionsHandled++;
            string dir = string.Format("{0}{1}", repoDIR, vssFile.Spec.Substring(1)).Replace("/", "\\");
            dir = Path.GetDirectoryName(dir);
            string filePath = Path.Combine(dir, vssFile.Name);
            VSSItem versionItem = vssFile.get_Version(vssVersion.VersionNumber);
            versionItem.Get(ref filePath, (int)(VSSFlags.VSSFLAG_USERRONO | VSSFlags.VSSFLAG_TIMEMOD));

            VssBindingRemover.RemoveBindings(filePath);

            if (isAdd)
            {
                if(!svnClient.Add(filePath))
                    return false;
            }

            var commitArgs = new SvnCommitArgs { LogMessage = versionItem.VSSVersion.Comment };
            if(!svnClient.Commit(filePath, commitArgs))
                return false;

            if (!string.IsNullOrEmpty(svnREVPROPSPATH))
            {
                SvnInfoEventArgs infoEventArgs;
                svnClient.GetInfo(SvnTarget.FromString(filePath), out infoEventArgs);
                String revPropsFolder = ((int)Math.Floor(infoEventArgs.Revision / 1000.0)).ToString();
                String propsFile = Path.Combine(Path.Combine(svnREVPROPSPATH, revPropsFolder),
                                             infoEventArgs.Revision.ToString());
                SvnRevProps props;
                try
                {
                    props = new SvnRevProps(propsFile);
                }
                catch (System.IO.FileNotFoundException) 
                {
                    // HACK: Possible hack for handling different structures in db\revprops.
                    //       It seems like not all repositories use the 'revpropsfolder' to split in thousands.
                    propsFile = Path.Combine(svnREVPROPSPATH,
                        infoEventArgs.Revision.ToString());
                    props = new SvnRevProps(propsFile);
                }
                props.SetAuthor(vssVersion.Username);
                props.SetDate(vssVersion.Date);
                props.Save();
            }

            File.Delete(filePath);
            return true;
        }

        private void Cleanup()
        {
            const string command = "cmd.exe";
            string parms = string.Format("/S /C rmdir /S /Q \"{0}\"", repoDIR);
            var startInfo = new ProcessStartInfo
                                {
                                    CreateNoWindow = true,
                                    UseShellExecute = false,
                                    FileName = command,
                                    Arguments = parms
                                };
            var proc = Process.Start(startInfo);
            if (proc != null)
            {
                proc.WaitForExit();
            }
        }

        private SvnClient GetSvnClient()
        {
            SvnClient svnClient = new SvnClient();
            SharpSvn.UI.SvnUIBindArgs bindargs = new SharpSvn.UI.SvnUIBindArgs();
            bindargs.ParentWindow = owner;
            SharpSvn.UI.SvnUI.Bind(svnClient,bindargs);
            svnClient.SvnError += new EventHandler<SvnErrorEventArgs>(svnClient_SvnError);

           return svnClient;
        }

        void svnClient_SvnError(object sender, SvnErrorEventArgs e)
        {
            //TODO: Implement some kind of graceful handling of authorization failures and IO (file exists etc) errors
            return;
        }
    }
} 